Stack machine

A stack machine may be

Contents

Practical Expression-Stack Machines

A stack machine implements a stack with registers. The operands of the ALU are always the top two registers of the stack and the result from the ALU is stored in the top register of the stack. 'Stack machine' commonly refers to computers which use a Last-in, First-out stack to hold short-lived temporary values while executing individual program statements. The instruction set carries out most ALU actions with postfix (Reverse Polish notation) operations that work only on the expression stack, not on data registers or main memory cells.

For a typical instruction like Add, both operands implicitly come from the topmost (most recent) values of the stack, and those two values get replaced by the result of the Add. The instruction's operands are 'popped' off the stack, and its result(s) are then 'pushed' back onto the stack, ready for the next instruction. Most stack instructions are encoded as just an opcode, with no additional fields to specify a register number or memory address or literal constant. This encoding is easily extended to richer operations with more than two inputs or more than one result. Integer constant operands are pushed by separate Load Immediate instructions. All accessing of program variables in main memory RAM is segregated into separate Load or Store instructions containing one memory address or some way to calculate that address from stacked operands.

The stack machine style is in contrast to register file machines which hold temporary values in a small fast visible array of similar registers, or accumulator machines which have only one visible general-purpose temp register, or memory-to-memory machines which have no visible temp registers.

Some machines have a stack of very limited size, implemented as a register file and a dynamic register renumbering scheme. Some machines have a stack of unlimited size, implemented as an array in RAM accessed by a 'top of stack' address register. Its topmost N values may be cached by invisible data registers for speed. [1][2] A few machines have both an expression stack in memory and a separate visible register stack.

Stack machines may have their expression stack and their call-return stack as separate things or as one integrated structure.

Some technical handheld calculators use reverse Polish notation in their keyboard interface, instead of having parenthesis keys. This is a form of stack machine. The Plus key relies on its two operands already being at the correct topmost positions of the user-visible stack.

Advantages of Stack Machine Instruction Sets

Very Compact Object Code

Stack machines have much smaller instructions than the other styles of machines. But operand loads are separate and so stack code requires roughly twice as many instructions as the equivalent code for register machines. The total code size (in bytes) is still less for stack machines.

In stack machine code, the most frequent instructions consist of just an opcode and can easily fit in 6 bits or less. Branches, load immediates, and load/store instructions require an argument field, but stack machines often arrange that the frequent cases of these still fit together with thin opcode into a single byte or syllable. The selection of operands from prior results is done implicitly by the ordering of the stack ops. In contrast, register machines require two or three register-number fields per ALU instruction to select its operands; the densest register machines average about 16 bits per instruction.

The instructions for accumulator or memory-to-memory machines are not padded out with multiple register fields. Instead, they use compiler-managed anonymous variables for subexpression values. These temps require extra memory reference instructions which take more code space than for the stack machine.

All stack machines have variants of the load/store opcodes for accessing local variables and formal parameters without explicit address calculations. This can be by offsets from the current top-of-stack address, or by offsets from a stable frame-base register. Register machines handle this with a register+offset address mode, but use a wider offset field.

Dense machine code was very valuable in the 1960s, when main memory was very expensive and very limited even on mainframes. It became important again on the initially-tiny memories of minicomputers and then microprocessors. Density remains important today, for smart phone apps and for Java apps downloaded into browsers over slow internet connections. Density also improves the effectiveness of caches and instruction prefetch cycles. Density allows smaller ROMs in embedded applications. But extreme density often comes with compromised program performance. And extreme performance often requires code that is several times bigger than stack code.

Some of the density of Burroughs B6700 code was due to moving vital operand information elsewhere, to 'tags' on every data word or into tables of pointers. The Add instruction itself was generic or polymorphic. It had to fetch the operand to discover whether this was an integer add or floating point add. The Load instruction could find itself tripping on an indirect address, or worse, a disguised call to a call-by-name thunk routine. The generic opcodes required fewer opcode bits but made the hardware more like an interpreter, with less opportunity to pipeline the common cases.

Simple Compilers

Compilers for stack machines are simpler and quicker to build than compilers for other machines. Code generation is trivial and independent of prior or subsequent code. It can be easily integrated into the parsing pass. No register management is needed, and no optimizations for constants or repeated simple memory references are needed (or even allowed). The same opcode that handles the frequent common case of Add, Indexed Load, or Function Call will also handle the general case involving complex subexpressions and nested calls. The compiler and the machine need not deal separately with corner cases.

This simplicity has allowed compilers to fit onto very small machines. The simple compilers allowed new product lines to get to market quickly, and allowed new operating systems to be written entirely in a new high level language rather than in assembly.[3][4] The UCSD p-System supported a complete student programming environment on early 8-bit microprocessors with poor instruction sets and little RAM, by compiling to a virtual stack machine rather than to the actual hardware.

The downside to the simplicity of compilers for stack machines, is that pure stack machines have not benefited much from subsequent advancements in compiler optimizer technology. However optimisation of compiled stack code is quite possible. Back-end optimisation of compiler output has been demonstrated to significantly improve code,[5][6] and potentially performance, whilst global optimisation within the compiler itself achieves further gains.[7]

Simple Interpreters

Some stack machine instruction sets are intended for interpretive execution of a virtual machine, rather than driving hardware directly. Interpreters for virtual stack machines are easier to build than interpreters for register or memory-to-memory machines; the logic for handling memory address modes is in just one place rather than repeated in many instructions. Stack machines also tend to have fewer variations of an opcode; one generalized opcode will handle both frequent cases and obscure corner cases of memory references or function call setup. (But code density is often improved by adding short and long forms for the same operation.)

Minimal Processor State

A machine with an expression stack can get by with just two visible registers, the top-of-stack address and the next-instruction address. The minimal hardware implementation has few bits of flipflops or registers. Faster implementations buffer the topmost N stack cells into invisible temp registers to reduce memory stack cycles.

Responding to an interrupt involves pushing the visible registers and branching to the interrupt handler. This is faster than storing most or all of the visible registers of a register machine, giving a quicker response to the interrupt.[8] Some register machines deal with this by having multiple register files that can be instantly swapped[9] but this increases its costs and slows down the register file.

Performance Disadvantages of Stack Machines

More Memory References

Some in the industry believe that stack machines execute more data cache cycles for temporary values and local variables than do register machines.[10] However, see [11] for a counter-argument which disproves many of the commonly held fallacies behind such arguments. The reader would do well to understand the conditions under which a stack machine can or cannot get away with pure stack versus mixed stack/memory access patterns.

On stack machines, temporary values often get spilled into memory, whereas on machines with many registers these temps usually remain in registers. (However, these values often need to be spilled into "activation frames" at the end of a procedure's definition, basic block, or at the very least, into a memory buffer during interrupt processing). Values spilled to memory add more cache cycles. This spilling effect depends on the number of hidden registers used to buffer top-of-stack values, upon the frequency of nested procedure calls, and upon host computer interrupt processing rates.

Some simple stack machines or stack interpreters use no top-of-stack hardware registers. Those minimal implementations are always slower than standard register machines. A typical expression like X+1 compiles to 'Load X; Load 1; Add'. This does implicit writes and reads of the memory stack which weren't needed:

for a total of 5 data cache references.

The next step up from this is a stack machine or interpreter with a single top-of-stack register. The above code then does:

for a total of 5 data cache references, worst-case. Generally, interpreters don't track emptiness, because they don't have to—anything below the stack pointer is a non-empty value, and the TOS cache register is always kept hot. Typical Java interpreters do not buffer the top-of-stack this way, however, because the program and stack have a mix of short and wide data values.

If the hardwired stack machine has N registers to cache the topmost memory stack words, then all spills and refills are avoided in this example and there is only 1 data cache cycle, the same as for a register or accumulator machine.

On register machines using optimizing compilers, it is very common for the most-used local variables to live in registers rather than in stack frame memory cells. This eliminates all data cache cycles for reading and writing those values, except for their initial load and final store upon procedure termination. The development of 'stack scheduling' for performing live-variable analysis, and thus retaining key variables on the stack for extended periods, goes a long way to answering this concern.

On the other hand, register machines must spill many of their registers to memory across nested procedure calls. The decision of which registers to spill, and when, is made statically at compile time rather than on the dynamic depth of the calls. This can lead to more data cache traffic than in an advanced stack machine implementation.

Factoring Out Common Subexpressions Has High Cost

In register machines, a subexpression which is used multiple times with the same result value can be evaluated just once and its result saved in a fast register. The subsequent reuses have no time or code cost, just a register reference that would have happened anyhow. This optimization wins for common simple expressions (e.g. loading variable X or pointer P) as well as less-common complex expressions.

With stack machines, in contrast, the results of a subexpression can be stored in one of two ways. The first way involves a temporary variable in memory. Storing and subsequent retrievals cost additional instructions and additional data cache cycles. Doing this is only a win if the subexpression computation costs more in time than fetching from memory, which in most stack CPUs, almost always is the case. It is never worthwhile for simple variables and pointer fetches, because those already have the same cost of one data cache cycle per access. It is only marginally worthwhile for expressions like X+1. These simpler expressions make up the majority of redundant, optimizable expressions in programs written in non-concatenative languages. An optimizing compiler can only win on redundancies that the programmer could have avoided in the source code.

The second way involves just leaving a computed value on the data stack, and duplicating it on an as-needed basis. This requires some amount of stack permutation, at the very least, an instruction to duplicate the results. This approach wins only if you can keep your data stack depth shallow enough for a "DUP", "ROT", or "OVER" type of instruction to gain access to the desired computed value. Some virtual machines support a general purpose permutation primitive, "PICK," which allows one to select arbitrarily any item in the data stack for duplication. Despite how limiting this approach sounds, hand-written stack code tends to make extensive use of this approach, resulting in software with runtime overheads comparable to those of general-purpose register-register architectures.[12][13] Unfortunately, algorithms for optimal "stack scheduling" of values aren't known to exist in general, making such stack optimizations difficult to impossible to automate for non-concatenative programming languages.

As a result, it is very common for compilers for stack machines to never bother applying code-factoring optimizations. It is too much trouble, despite the significant payoff.

Rigid Code Order

In modern machines, the time to fetch a variable from the data cache is often several times longer than the time needed for basic ALU operations. A program runs faster without stalls if its memory loads can be started several cycles before the instruction which needs that variable, while also working on independent instructions. Complex machines can do that with a deep pipeline and "out-of-order execution" that examines and runs many instructions at once. Register machines can also do this with much simpler "in-order" hardware, a shallow pipeline, and slightly smarter compilers.[14] The load step becomes a separate instruction, and that instruction is statically scheduled much earlier in the code sequence. The compiler puts independent steps in between.

This scheduling trick requires explicit, spare registers. It is not possible on stack machines without exposing some aspect of the micro-architecture to the programmer. For the expression A-B, the right operand must be evaluated and pushed immediately prior to the Minus step. Without stack permutation or hardware multithreading, relatively little useful code can be put in between while waiting for the Load B to finish. Stack machines can work around the memory delay by either having a deep out-of-order execution pipeline covering many instructions at once, or more likely, they can permute the stack such that they can work on other workloads while the load completes, or they can interlace the execution of different program threads, as in the Unisys A9 system.[15] Today's increasingly parallel computational loads suggests, however, this might not be the disadvantage it's been made out to be in the past.

Hides a Faster Register Machine Inside

Some simple stack machines have a chip design which is fully customized all the way down to the level of individual registers. The top of stack address register and the N top of stack data buffers are built from separate individual register circuits, with separate adders and ad hoc connections.

However, most stack machines are built from larger circuit components where the N data buffers are stored together within a register file and share read/write buses. The decoded stack instructions are mapped into one or more sequential actions on that hidden register file. Loads and ALU ops act on a few topmost registers, and implicit spills and fills act on the bottommost registers. The decoder allows the instruction stream to be compact. But if the code stream instead had explicit register-select fields which directly manipulated the underlying register file, the compiler could make better use of all registers and the program would run faster.

Microprogrammed stack machines are an example of this. The inner microcode engine is some kind of RISC-like register machine or a VLIW-like machine using multiple register files. When controlled directly by task-specific microcode, that engine gets much more work completed per cycle than when controlled indirectly by equivalent stack code for that same task.

The object code translators for the HP 3000 and Tandem T/16 are another example. [16] [17] They translated stack code sequences into equivalent sequences of RISC code. Minor 'local' optimizations removed much of the overhead of a stack architecture. Spare registers were used to factor out repeated address calculations. The translated code still retained plenty of emulation overhead from the mismatch between original and target machines. Despite that burden, the cycle efficiency of the translated code matched the cycle efficiency of the original stack code. And when the source code was recompiled directly to the register machine via optimizing compilers, the efficiency doubled. This shows that the stack architecture and its non-optimizing compilers were wasting over half of the power of the underlying hardware.

Register files are good tools for computing because they have high bandwidth and very low latency, compared to memory references via data caches. In a simple machine, the register file allows reading two independent registers and writing of a third, all in one ALU cycle with one-cycle or less latency. Whereas the corresponding data cache can start only one read or one write (not both) per cycle, and the read typically has a latency of two ALU cycles. That's one third of the throughput at twice the pipeline delay. In a complex machine like Athlon that completes two or more instructions per cycle, the register file allows reading of four or more independent registers and writing of two others, all in one ALU cycle with one-cycle latency. Whereas the corresponding dual-ported data cache can start only two reads or writes per cycle, with multiple cycles of latency. Again, that's one third of the throughput of registers. It is very expensive to build a cache with additional ports.

More Instructions, Slower Interpreters

Interpreters for virtual stack machines are often slower than interpreters for other styles of virtual machine. This slowdown is worst when running on host machines with deep execution pipelines, such as current x86 chips.

A program has to execute more instructions when compiled to a stack machine than when compiled to a register machine or memory-to-memory machine. Every variable load or constant requires its own separate Load instruction, instead of being bundled within the instruction which uses that value. The separated instructions may be simple and faster running, but the total instruction count is still higher.

In every interpreter, the interpreter must execute a N-way switch jump to decode the next opcode and branch to its steps for that particular opcode. The host machine's prefetch mechanisms are unable to predict and fetch the target of that indexed or indirect jump. So the host machine's execution pipeline must restart each time the hosted interpreter decodes another virtual instruction. This happens more often for virtual stack machines than for other styles of virtual machine. [18][19] Android's Dalvik virtual machine for Java uses a virtual-register 16-bit instruction set instead of Java's usual 8-bit stack code, to minimize instruction count and opcode dispatch stalls. Arithmetic instructions directly fetch or store local variables via 4-bit (or larger) instruction fields.

Hybrid Machines

Pure stack machines are quite inefficient for procedures which access multiple fields from the same object. The stack machine code must reload the object pointer for each pointer+offset calculation. A common fix for this is to add some register-machine features to the stack machine: a visible register file dedicated to holding addresses, and register-style instructions for doing loads and simple address calculations. It is uncommon to have the registers be fully general purpose, because then there is no strong reason to have an expression stack and postfix instructions.

Another common hybrid is to start with a register machine architecture, and add another memory address mode which emulates the push or pop operations of stack machines: 'memaddress = reg; reg += instr.displ'. This was first used in DEC's PDP-11 minicomputer. This feature was carried forward in VAX computers and in Motorola 6800 and M68000 microprocessors. This allowed the use of simpler stack methods in early compilers. It also efficiently supported virtual machines using stack interpreters or threaded code. However, this feature did not help the register machine's own code to become as compact as pure stack machine code. And the execution speed was less than when compiling well to the register architecture. It is faster to change the top-of-stack pointer only occasionally (once per call or return) rather than constantly stepping it up and down throughout each program statement. And even faster to avoid memory references entirely.

More recently, so-called second-generation stack machines have adopted a dedicated collection of registers to serve as address registers, off-loading the task of memory addressing from the data stack. For example, MuP21 relies on a register called "A", while the more recent GreenArrays processors relies on two registers: A and B.[20]

The Intel x86 family of microprocessors have a register-style instruction set for most operations, but use stack instructions for its oldest x87 form of floating point arithmetic.

Commercial stack machines

Examples of stack instruction sets directly executed in hardware include

Virtual stack machines

Examples of virtual stack machines interpreted in software:

See also

Computers Using Call Stacks and Stack Frames

Most current computers (of any instruction set style) and most compilers use a large call-return stack in memory to organize the short-lived local variables and return links for all currently active procedures or functions. Each nested call creates a new stack frame in memory, which persists until that call completes. This call-return stack may be entirely managed by the hardware via specialized address registers and special address modes in the instructions. Or it may be merely a set of conventions followed by the compilers, using generic registers and register+offset address modes. Or it may be something in between.

Since this technique is now nearly universal, even on register machines, it is not helpful to refer to all these machines as stack machines. That term is commonly reserved for machines which also use an expression stack and stack-only arithmetic instructions to evaluate the pieces of a single statement.

Computers commonly provide direct, efficient access to the program's global variables and to the local variables of only the current innermost procedure or function, the topmost stack frame. 'Up level' addressing of the contents of callers' stack frames is usually not needed and not supported as directly by the hardware. If needed, compilers support this by passing in frame pointers as additional, hidden parameters.

Some Burroughs stack machines do support up-level refs directly in the hardware, with specialized address modes and a special 'display' register file holding the frame addresses of all outer scopes. No subsequent computer lines have done this in hardware. When Niklaus Wirth developed the first Pascal compiler on a CDC 6000, he found that it was faster overall to pass in the frame pointers as a chain, rather than constantly updating complete arrays of frame pointers. This software method also adds no overhead for common languages like C which lack up-level refs.

The same Burroughs machines also supported nesting of tasks or threads. The task and its creator share the stack frames that existed at the time of task creation, but not the creator's subsequent frames nor the task's own frames. This was supported by a cactus stack, whose layout diagram resembled the trunk and arms of a Saguaro cactus. Each task had its own memory segment holding its stack and the frames that it owns. The base of this stack is linked to the middle of its creator's stack. In machines with a conventional flat address space, the creator stack and task stacks would be separate heap objects in one heap.

In some programming languages, the outer-scope data environments are not always nested in time. These languages organize their procedure 'activation records' as separate heap objects rather than as stack frames appended to a linear stack.

In simple languages like Forth that lack local variables and naming of parameters, stack frames would contain nothing more than return branch addresses and frame management overhead. So their return stack holds bare return addresses rather than frames. The return stack is separate from the data value stack, to improve the flow of call setup and returns.

Stack Machines in Automata Theory

In the automata theory branch of theoretical computer science, a 'stack machine' is a hypothetical rudimentary computer that lacks random-access memory but does have one or more pure LIFO stacks for data. This is used in theories about what can be computed. (No practical computers are built this way. In all real computers or useful virtual computers, the bulk of active data memory is physically a large RAM.)

The computer's input is the initial content of stack 1; all the other stacks start empty. Each state of a stack machine is either a read state or a write state; and each state specifies a stack number to read (pop) from, or write (push) onto. In addition, a write state specifies the symbol to write, and the next state to transition to. A read state specifies, for each symbol in the alphabet, what state it would transition to if that symbol were read; in addition, it also specifies what state to transition to if the stack were empty. A stack machine halts when it transitions into a special halting state.

A stack machine with 1 stack is a very weak model of computation. For example, it can be shown that no 1-stack stack machine can recognize the simple language 0n1n0n (a number of 0s followed by the same number of 1s followed by the same number of 0s), via pumping arguments. The computational power of 1-stack stack machines is strictly greater than that of finite automata, (as seen by noting that it can recognize but 0n1n which cannot be recognized by a finite autotmata). The 1-stack machine is strictly less powerful than that of deterministic pushdown automata.

A stack machine with multiple stacks, on the other hand, is equivalent to a Turing machine. For example, a 2-stack machine can emulate a Turing machine by using one stack for the tape portion to the left of the Turing machine's current head position and the other stack for the portion to the right. And a Turing machine can, with near-infinite amounts of time, emulate any conceivable deterministic computer of any design.

References

  1. ^ Burroughs large systems
  2. ^ HP 3000
  3. ^ Burroughs large systems
  4. ^ HP 3000
  5. ^ Koopman, Philip (1994). "A Preliminary Exploration of Optimized Stack Code Generation". Journal of Forth applications and Research 6 (3). http://www.ece.cmu.edu/~koopman/stack_compiler/stack_co.pdf. 
  6. ^ Bailey, Chris (2000). "Inter-Boundary Scheduling of Stack Operands: A preliminary Study". Proceedings of Euroforth 2000 Conference. http://www.complang.tuwien.ac.at/anton/euroforth/ef00/bailey00.pdf. 
  7. ^ Shannon, Mark; Bailey C (2006). "Global Stack Allocation : Register Allocation for Stack Machines". Proceedings of Euroforth Conference 2006. http://www.complang.tuwien.ac.at/anton/euroforth2006/papers/shannon.pdf. 
  8. ^ RTX2010
  9. ^ 8051 CPU Manual, Intel, 1980
  10. ^ "Computer Architecture: A Quantitative Approach", John L Hennessy, David A Patterson; See the discussion of stack machines.
  11. ^ http://www.eecg.utoronto.ca/~laforest/Second-Generation_Stack_Computer_Architecture.pdf Second-Generation Stack Computer Architecture
  12. ^ http://www.ece.cmu.edu/~koopman/stack_computers/ Stack Computers: the new wave book by Philip J. Koopman, Jr. 1989
  13. ^ http://www.eecg.utoronto.ca/~laforest/Second-Generation_Stack_Computer_Architecture.pdf Second-Generation Stack Computer Architecture
  14. ^ Hennessy, ibid.
  15. ^ 'Introduction to A Series Systems', Burroughs Corporation, page 41, http://www.bitsavers.org/pdf/burroughs/A-Series/1170057_aSeries_Intro_apr86.pdf
  16. ^ HP3000 Emulation on HP Precision Architecture Computers, Arndt Bergh, Keith Keilman, Daniel Magenheimer, and James Miller, Hewlett-Packard Journal, Dec 1987, p87-89, http://www.hpl.hp.com/hpjournal/pdfs/IssuePDFs/1987-12.pdf
  17. ^ Migrating a CISC Computer Family onto RISC via Object Code Translation, K. Andrews and D. Sand, Proceedings of ASPLOS-V, October, 1992
  18. ^ "Virtual Machine Showdown: Stack vs. Register Machine", Yunhe Shi, David Gregg, Andrew Beatty, M. Anton Ertle, http://usenix.org/events/vee05/full_papers/p153-yunhe.pdf
  19. ^ 'The Case for Virtual Register Machines', Brian Davis, Andrew Beatty, Kevin Casey, David Gregg and John Waldron, http://www.scss.tcd.ie/David.Gregg/papers/Gregg-SoCP-2005.pdf
  20. ^ http://www.eecg.utoronto.ca/~laforest/Second-Generation_Stack_Computer_Architecture.pdf Second-Generation Stack Computer Architecture
  21. ^ "The World's First Java Processor", by David A. Greve and Matthew M. Wilding, Electronic Engineering Times, Jan. 12, 1998,
  22. ^ 'MARC4 4-bit Microcontrollers Programmers Guide', Atmel, http://www.atmel.com/dyn/resources/prod_documents/doc4747.pdf
  23. ^ Forth chips
  24. ^ F21 Microprocessor Overview
  25. ^ PSC1000 Microprocessor Reference Manual, Patriot Scientific Corporation, http://www.forthfreak.net/misc/psc1000.pdf
  26. ^ A Java chip available — now!, by Rick Brian Slack
  27. ^ 4stack Processor
  28. ^ 'Porting the GNU C Compiler to the Thor Microprocessor', Harry Gunnarsson and Thomas Lundqvist, http://lundqvist.dyndns.org/Publications/thesis95/ThorGCC.pdf

External links